package com.sunyard.utils.sm2;

import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;

public class PKCS12_SM2 {
    private ASN1Sequence privateInfo = null;
    private ASN1Sequence publicInfo = null;
    private String SM2PrivateKey;
    private X509Cert_SM2[] certs;

    public static PKCS12_SM2 getInstance(Object obj)
            throws Exception {
        if ((obj instanceof PKCS12_SM2))
            return (PKCS12_SM2) obj;
        if ((obj instanceof byte[]))
            return new PKCS12_SM2((byte[]) obj);
        if (obj != null) {
            return new PKCS12_SM2(ASN1Sequence.getInstance(obj));
        }
        return null;
    }

    public PKCS12_SM2() {
    }

    public PKCS12_SM2(byte[] encoding) throws Exception {
        if (encoding == null) {
            throw new Exception("SM2File encoding should not be null");
        }
        load(encoding);
    }

    public PKCS12_SM2(ASN1Sequence seq) throws Exception {
        parseSM2(seq);
    }

    public PKCS12_SM2(ASN1Sequence publicInfo, ASN1Sequence privateInfo) throws Exception {
        if (publicInfo == null) {
            throw new Exception("SM2File publicInfo should not be null");
        }

        if (privateInfo == null) {
            throw new Exception("SM2File privateInfo should not be null");
        }

        parseSM2Certs(privateInfo, publicInfo);
    }

    public void load(byte[] data) throws Exception {
        ASN1Sequence seq;
        try {
            data = Base64.decode(data);
            seq = ASN1Sequence.getInstance(data);
        } catch (Exception e) {
            throw new Exception("SM2File Decoding failure", e);
        }

        parseSM2(seq);
    }

    public void parseSM2(ASN1Sequence seq) throws Exception {
        if ((seq == null) || (seq.size() != 3)) {
            throw new Exception("invalid SM2File encoding");
        }

        parseSM2Certs((ASN1Sequence) seq.getObjectAt(1), (ASN1Sequence) seq.getObjectAt(2));
    }

    private void parseSM2Certs(ASN1Sequence privateInfo, ASN1Sequence publicInfo) throws Exception {
        if (privateInfo.size() != 3) {
            throw new Exception("the sm2 file is not right format,can not get the private part");
        }
        if (publicInfo.size() != 2) {
            throw new Exception("the sm2 file is not right format.can not get the public part");
        }
        this.privateInfo = privateInfo;
        this.publicInfo = publicInfo;

        ASN1OctetString pubOctString = (ASN1OctetString) publicInfo.getObjectAt(1);

        this.certs = new X509Cert_SM2[]{new X509Cert_SM2(pubOctString.getOctets())};

        System.out.println(certs[0].toString());
    }

    private static byte[] KDF(byte[] z) {
        byte[] ct = {0, 0, 0, 1};
        SM3Digest sm3 = new SM3Digest();
        sm3.update(z, 0, z.length);
        sm3.update(ct, 0, ct.length);
        byte[] hash = new byte[32];
        sm3.doFinal(hash, 0);
        return hash;
    }

    public String getPrivateKey() throws Exception {
        if (this.SM2PrivateKey == null) {
            //throw new Exception(Exception.DECRYPT_P12_ERR, Exception.DECRYPT_P12_ERR_DES);
            return null;
        }
        return this.SM2PrivateKey;
    }

//	  public byte[] getPrivateKey(String password,ASN1Sequence privateInfo) throws Exception {
//	       return decrypt(password,privateInfo);
//	  }

    public BigInteger getPrivateKey(String password) throws Exception {
        return decrypt(password);
    }

    public BigInteger decrypt(String password) throws Exception {

        if (password == null) {
            throw new Exception("SM2File password should not be null");
        }

        if (privateInfo == null) {
            throw new Exception("SM2File invalid : privateInfo=null");
        }

        ASN1OctetString priOctString = (ASN1OctetString) privateInfo.getObjectAt(2);

        byte[] encryptedData;
        try {
            encryptedData = priOctString.getOctets();
        } catch (Exception e) {
            throw new Exception("SM2File decoding failure", e);
        }

        String dBytes = SM4DecryptDBytes(password, encryptedData);

        return new BigInteger(dBytes, 16);
    }

//	  public byte[] decrypt(String password,ASN1Sequence privateInfo) throws Exception
//	  {
////	    if (this.SM2PrivateKey == null) {
//	    	
//	      if (password == null) {
//	        throw new Exception("SM2File password should not be null");
//	      }
//	      
//	      if (privateInfo == null) {
//	        throw new Exception("SM2File invalid : privateInfo=null");
//	      }
//	      
//	      ASN1OctetString priOctString = (ASN1OctetString)privateInfo.getObjectAt(2);
//	      
//	      byte[] encryptedData;
//	      try {
//	        encryptedData = priOctString.getOctets();
//	      } catch (Exception e) {
//	        throw new Exception("SM2File decoding failure", e);
//	      }
//
//	      byte[] dBytes = SM4DecryptDBytes(password, encryptedData);
//
//	      X509Cert_SM2 cert = getPublicCert()[0];
//	      SM2PublicKey pubKey = (SM2PublicKey)cert.getPublicKey();
//	      byte[] pubX = pubKey.getPubXByBytes();
//	      byte[] pubY = pubKey.getPubYByBytes();
//
////	      this.SM2PrivateKey = new SM2PrivateKey(dBytes, pubX, pubY);
////	    }
//
//	    return dBytes;
//	  }

    private final String SM4DecryptDBytes(String password, byte[] encryptedData) throws Exception {
        if ((password == null) || (password.length() == 0))
            throw new Exception("SM2File password should not be null");
        byte[] passwordBytes;
        try {
            passwordBytes = password.getBytes("UTF8");
        } catch (UnsupportedEncodingException e) {
            throw new Exception("SM2File password decoding failure", e);
        }

        if ((encryptedData == null) || (encryptedData.length == 0)) {
            throw new Exception("SM2File encryptedData should not be null");
        }

        if ((encryptedData.length < 32) || (encryptedData.length > 64)) {
            throw new Exception("SM2File EncryptedData required length in [32-64] ");
        }

        byte[] encoding = null;
        if ((encryptedData.length == 32) || (encryptedData.length == 48))
            encoding = encryptedData;
        else
            try {
                encoding = Base64.decode(encryptedData);
            } catch (Exception e) {
                throw new Exception("SM2File EncryptedData required base64 ");
            }
        byte[] iv;
        byte[] sm4;
        try {
            byte[] hash = KDF(passwordBytes);
            iv = new byte[16];
            System.arraycopy(hash, 0, iv, 0, 16);
            sm4 = new byte[16];
            System.arraycopy(hash, 16, sm4, 0, 16);
        } catch (Exception e) {
            throw new Exception("SM2File KDF failure", e);
        }
        try {
            PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new SM4Engine()), new PKCS7Padding());
            ParametersWithIV params = new ParametersWithIV(new KeyParameter(sm4), iv);
            cipher.init(false, params);

            int outLength = cipher.getOutputSize(encoding.length);
            byte[] out = new byte[outLength];
            int dataLength = cipher.processBytes(encoding, 0, encoding.length, out, 0);
            int lastLength = cipher.doFinal(out, dataLength);
            int realLength = dataLength + lastLength;
            byte[] dBytes = null;
            if (realLength < outLength) {
                dBytes = new byte[realLength];
                System.arraycopy(out, 0, dBytes, 0, realLength);
            } else {
                dBytes = out;
            }
            System.out.println("dBytes: " + Hex.toHexString(dBytes));
            // dBytes1 = [108, -106, 69, 63, -40, 7, 75, -12, 99, -33, -35, 9, -21, -33, -9, -125, -22, -23, -101, -59, 3, 96, 41, -13, -13, 65, 5, 5, 113, 15, 23, -52];

            return Hex.toHexString(dBytes);
        } catch (DataLengthException e) {
            throw new Exception("SM2File SM2PrivateKey decrypt failure with IllegalDataLength", e);
        } catch (IllegalArgumentException e) {
            throw new Exception("SM2File SM2PrivateKey decrypt failure with IllegalArgument", e);
        } catch (IllegalStateException e) {
            throw new Exception("SM2File SM2PrivateKey decrypt failure with IllegalState", e);
        } catch (InvalidCipherTextException e) {
            throw new Exception("SM2File SM2PrivateKey decrypt failure with InvalidCipherText", e);
        } catch (Exception e) {
            throw new Exception("SM2File SM2PrivateKey decrypt failure", e);
        }
    }

//	  private static final byte[] SM4EncryptDBytes(String password, byte[] privateKeyData)
//	    throws Exception
//	  {
//	    if ((password == null) || (password.length() == 0))
//	      throw new Exception("SM2File password should not be null");
//	    byte[] passwordBytes;
//	    try
//	    {
//	      passwordBytes = password.getBytes("UTF8");
//	    } catch (UnsupportedEncodingException e) {
//	      throw new Exception("SM2File password decoding failure", e);
//	    }
//
//	    if ((privateKeyData == null) || (privateKeyData.length == 0))
//	      throw new Exception("SM2File EncryptedData should not be null");
//	    byte[] iv;
//	    byte[] sm4;
//	    try {
//	      byte[] hash = KDF(passwordBytes);
//	      iv = new byte[16];
//	      System.arraycopy(hash, 0, iv, 0, 16);
//	      sm4 = new byte[16];
//	      System.arraycopy(hash, 16, sm4, 0, 16);
//	    } catch (Exception e) {
//	      throw new Exception("SM2File KDF failure", e);
//	    }
//	    try
//	    {
//	      PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new SM4Engine()), new PKCS7Padding());
//	      ParametersWithIV params = new ParametersWithIV(new KeyParameter(sm4), iv);
//	      cipher.init(true, params);
//
//	      int outLength = cipher.getOutputSize(privateKeyData.length);
//	      byte[] out = new byte[outLength];
//	      int dataLength = cipher.processBytes(privateKeyData, 0, privateKeyData.length, out, 0);
//	      int lastLength = cipher.doFinal(out, dataLength);
//	      int realLength = dataLength + lastLength;
//
//	      byte[] encryptedData = null;
//	      if (realLength < outLength) {
//	        encryptedData = new byte[realLength];
//	        System.arraycopy(out, 0, encryptedData, 0, realLength);
//	      } else {
//	        encryptedData = out;
//	      }
//	      return encryptedData;
//	    } catch (DataLengthException e) {
//	      throw new Exception("SM2File SM2PrivateKey encrypt failure with IllegalDataLength", e);
//	    } catch (IllegalArgumentException e) {
//	      throw new Exception("SM2File SM2PrivateKey encrypt failure with IllegalArgument", e);
//	    } catch (IllegalStateException e) {
//	      throw new Exception("SM2File SM2PrivateKey encrypt failure with IllegalState", e);
//	    } catch (InvalidCipherTextException e) {
//	      throw new Exception("SM2File SM2PrivateKey encrypt failure with InvalidCipherText", e); } catch (Exception e) {
//	    }
//	    throw new Exception("SM2File SM2PrivateKey encrypt failure", e);
//	  }

    public X509Cert_SM2[] getPublicCert() throws Exception {
        if (this.certs == null) {
            throw new Exception("SM2File invalid : certs=null");
        }

        return this.certs;
    }
//
//	  public ASN1Primitive toASN1Primitive()
//	  {
//	    if (this.privateInfo == null) {
//	      throw new IllegalArgumentException("SM2File privateInfo should not be null");
//	    }
//	    if (this.publicInfo == null) {
//	      throw new IllegalArgumentException("SM2File publicInfo should not be null");
//	    }
//
//	    ASN1EncodableVector v = new ASN1EncodableVector();
//	    v.add(new ASN1Integer(1L));
//	    v.add(this.privateInfo);
//	    v.add(this.publicInfo);
//	    return new BERSequence(v);
//	  }
//
//	  private static PKCS12_SM2 generateSM2(X509Cert x509Cert, PrivateKey privateKey, String password) throws Exception {
//	    if ((password == null) || (password.length() == 0)) {
//	      throw new Exception("SM2File password should not be null");
//	    }
//	    if (privateKey == null) {
//	      throw new Exception("SM2File privateKey should not be null");
//	    }
//	    if (x509Cert == null) {
//	      throw new Exception("SM2File x509Cert should not be null");
//	    }
//
//	    SM2PrivateKey sm2PrvKey = null;
//	    if ((privateKey instanceof SM2PrivateKey))
//	      sm2PrvKey = (SM2PrivateKey)privateKey;
//	    else {
//	      throw new Exception("SM2File privateKey must SM2PrivateKey"); } Session softLib = BCSoftLib.INSTANCE();
//	    Mechanism mechanism = new Mechanism("sm3WithSM2Encryption");
//	    byte[] sourceData = "TESTING".getBytes();
//	    boolean verifyResult;
//	    try { byte[] signData = softLib.sign(mechanism, sm2PrvKey, sourceData);
//	      verifyResult = softLib.verify(mechanism, x509Cert.getPublicKey(), sourceData, signData);
//	    } catch (Exception e) {
//	      throw new Exception("SM2File x509Cert/privateKey try signing failure", e);
//	    }
//
//	    if (!verifyResult) {
//	      throw new Exception("SM2File x509Cert/privateKey not match");
//	    }
//
//	    byte[] dBytes = sm2PrvKey.getDByBytes();
//	    if ((dBytes == null) || (dBytes.length != 32)) {
//	      throw new Exception("SM2File SM2PrivateKey format invalid");
//	    }
//
//	    byte[] encryptedData = SM4EncryptDBytes(password, sm2PrvKey.getDByBytes());
//	    try
//	    {
//	      ASN1EncodableVector publicInfoVector = new ASN1EncodableVector();
//	      publicInfoVector.add(PKCSObjectIdentifiers.sm2Data);
//	      DEROctetString pubDEROctetString = new DEROctetString(x509Cert.getEncoding());
//	      publicInfoVector.add(pubDEROctetString);
//	      DERSequence publicInfo = new DERSequence(publicInfoVector);
//
//	      ASN1EncodableVector privateInfoVector = new ASN1EncodableVector();
//	      privateInfoVector.add(PKCSObjectIdentifiers.sm2Data);
//	      privateInfoVector.add(PKCSObjectIdentifiers.SM4_CBC);
//	      DEROctetString prvDEROctetString = new DEROctetString(encryptedData);
//	      privateInfoVector.add(prvDEROctetString);
//	      DERSequence privateInfo = new DERSequence(privateInfoVector);
//
//	      return new PKCS12_SM2(publicInfo, privateInfo); } catch (Exception e) {
//	    }
//	    throw new Exception("SM2File Generated failure", e);
//	  }
//
//	  public static String generateSM2File(X509Cert x509Cert, PrivateKey privateKey, String password, String fileName)
//	    throws Exception
//	  {
//	    if (fileName == null) {
//	      throw new Exception("SM2File fileName should not be null");
//	    }
//
//	    byte[] encoding = generateSM2Data(x509Cert, privateKey, password);
//	    try
//	    {
//	      FileHelper.write(fileName, encoding);
//	    } catch (IOException e) {
//	      throw new Exception("Writing SM2File failure with IOException", e);
//	    }
//	    return fileName;
//	  }
//
//	  public static byte[] generateSM2Data(X509Cert x509Cert, PrivateKey privateKey, String password)
//	    throws Exception
//	  {
//	    byte[] encoding = CombineSM2Data(x509Cert, privateKey, password);
//
//	    if (SM2ContextConfig.getBase64State()) {
//	      encoding = Base64.encode(encoding);
//	    }
//	    return encoding;
//	  }
//
//	  public static byte[] CombineSM2Data(X509Cert x509Cert, PrivateKey privateKey, String password) throws Exception
//	  {
//	    PKCS12_SM2 p12_SM2 = generateSM2(x509Cert, privateKey, password);
//	    byte[] encoding;
//	    try {
//	      ByteArrayOutputStream baos = new ByteArrayOutputStream();
//	      DEROutputStream out = new DEROutputStream(baos);
//	      out.writeObject(p12_SM2);
//	      encoding = baos.toByteArray();
//	    } catch (IOException e) {
//	      throw new Exception("Encoding SM2File failure with IOException", e);
//	    }
//
//	    return encoding;
//	  }
}
